package com.yxq.sso.Service.impl;

import com.yxq.sso.Service.UserService;
import com.yxq.sso.common.ExceptionCode;
import com.yxq.sso.controller.UserController;
import com.yxq.sso.entity.Menu;
import com.yxq.sso.entity.MenuVo;
import com.yxq.sso.entity.Role;
import com.yxq.sso.entity.User;
import com.yxq.sso.exception.BusinessException;
import com.yxq.sso.mapper.UserMapper;
import com.yxq.sso.repository.UserRepository;
import com.yxq.sso.util.AuthTreeUtil;
import com.yxq.sso.util.JsonUtil;
import com.yxq.sso.util.MD5Util;
import com.yxq.sso.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * 用户一键登录或注册的实现类
 *
 * @author YXQ
 * @date 2021/11/30 9:28
 */
@Service("userLoginService")
public class UserServiceImpl implements UserService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private UserRepository userRepository;

    /**
     * 用户的权限菜单的最终版
     */
    List<Menu> userRoleMenu = new ArrayList<>();

    @Autowired
    UserMapper userMapper;

    /**
     * 需要在用户注册时记录远程的ip地址,用request的remote相关方法,service中就需要得到HttpServletRequest对象
     * 在spring和springboot中可直接对request实现注入/装配
     * 这个注入可以有其他办法：
     * 1.Controller方法参数带上HttpServletRequest,再通过，构造器，或者方法将request传递到service
     * 2.用Resource注解，变量名字需要保持一致private HttpServletRequest httpServletRequest;
     * request在调用方法时装配
     */
    @Autowired
    private HttpServletRequest request;

    /**
     * 设置token前缀,防止被黑客抓包得到token
     */
    public static final String LOGIN = "#Zhao-Yang#=Mi+Cai=";

    @Override
    public Map<String, Object> login(String phone, String code, Integer loginType) throws BusinessException {
        // 一键登录
        // 1.获取redis服务器存储的验证码
        ValueOperations<String, Object> stringOps = redisTemplate.opsForValue();
        Object o = stringOps.get(SmsServiceImpl.LOGIN_SMS + phone);
        // 验证码失效的判断,如果为空则抛出异常
        if (o == null) {
            // 帐号或短信验证码错误，请重新输入
            throw new BusinessException(ExceptionCode.PHONE_NUMBER_OR_CODE_FAILED);
        }
        // 转化验证码为字符串
        String redisCode = o.toString();
        // 对验证码做校验, 如果不一致则抛出异常
        if (!redisCode.equals(code)) {
            // 帐号或短信验证码错误，请重新输入
            throw new BusinessException(ExceptionCode.PHONE_NUMBER_OR_CODE_FAILED);
        }
        // 校验完成, 从mysql数据库中查找用户信息
        // 通过手机号查询用户是否存在findByPhoneEquals
        User findUser = userRepository.findByPhoneEquals(phone);
        // 删除验证码,防止验证码被多次使用
        redisTemplate.delete(SmsServiceImpl.LOGIN_SMS + phone);
        //如果用户不存在, 则匿名注册,匿名格式: "用户:"+phone , 并指定用户状态
        if (findUser == null) {
            throw new BusinessException(ExceptionCode.NOT_REGISTER);
            /*
            findUser = new User();
            // 在注册时才需要的字段
            findUser.setName("用户" + phone);
            findUser.setLoginType(loginType);
            findUser.setStatus(1);
            findUser.setPhone(phone);
             */
        }

        //如果用户存在, 但状态不为1,表示冻结状态, 抛出异常
        if (findUser.getStatus() != 1) {
            throw new BusinessException(ExceptionCode.WRONG_LOGIN_STATUS);
        }

        // 如果用户存在,获取上次的token,如果不为空移除redis中上一次的登录信息
        String expireToken = findUser.getToken();
        if (!StringUtils.isEmpty(expireToken)) {
            redisTemplate.delete(expireToken);
        }
        // 执行登录流程，更新登录的ip地址/token/最近一次登录的时间
        findUser.setIpAddress(request.getRemoteAddr());
        findUser.setLastLoginTime(new Date());
        // 生成token，存储登录信息(json),利用处理后的UUID
        String token = UUID.randomUUID().toString().replace("-", "");
        // 处理登录
        // 设置ip,设置token,更新mysql
        findUser.setToken(LOGIN + token);
        // 执行添加或更新
        userRepository.save(findUser);
        // redis添加token信息和用户信息以及
        // 过期时间：pc端2小时，手机端1个月，手机端的H5也是2个小时
        findUser.setLoginType(loginType);
        // 1.pc 2.小程序
        if (findUser.getLoginType() == 1) {
            // TODO:过期时间：pc端2小时，手机端1个月，手机端的H5也是2个小时
            stringOps.set(LOGIN + token, JsonUtil.objectToString(findUser), 2, TimeUnit.HOURS);
        } else {
            stringOps.set(LOGIN + token, JsonUtil.objectToString(findUser), 30, TimeUnit.DAYS);
        }

        List<MenuVo> userMenuVoTree = getUserMenuVoTree(findUser);

        Map<String, Object> result = new HashMap<>(3);
        // 放入token
        result.put("token", token);
        result.put("menu", userMenuVoTree);
        result.put("user", findUser);
        return result;
    }

    @Override
    public Map<String, Object> loginByPassword(String phone, String password, Integer loginType) throws BusinessException {
        // 一键登录
        // 1.获取redis服务器存储的验证码
        ValueOperations<String, Object> stringOps = redisTemplate.opsForValue();
        // 先获取用户的盐
        User user = userRepository.findByPhoneEquals(phone);
        if (user == null) {
            // 您还未注册，请先去注册
            throw new BusinessException(ExceptionCode.NOT_REGISTER);
        }
        // 获取当前用户的盐值
        String salt = user.getSalt();
        // 对密码进行加密
        String encode = MD5Util.encode(password + salt);
        // 校验完成, 从mysql数据库中查找用户信息
        // 通过手机号和密码查询用户是否存在 findByPhoneAndPassword
        User findUser = userRepository.findByPhoneAndPassword(phone, encode);
        //如果用户不存在, 则匿名注册,匿名格式: "用户:"+phone , 并指定用户状态
        if (findUser == null) {
            // 您还未注册，请先去注册
            throw new BusinessException(ExceptionCode.NOT_REGISTER);
        }

        //如果用户存在, 但状态不为1,表示冻结状态, 抛出异常
        if (findUser.getStatus() != 1) {
            throw new BusinessException(ExceptionCode.WRONG_LOGIN_STATUS);
        }

        // 如果用户存在,获取上次的token,如果不为空移除redis中上一次的登录信息
        String expireToken = findUser.getToken();
        if (!StringUtils.isEmpty(expireToken)){
            redisTemplate.delete(expireToken);
        }
        // 执行登录流程，更新登录的ip地址/token/最近一次登录的时间
        findUser.setIpAddress(request.getRemoteAddr());
        findUser.setLastLoginTime(new Date());
        // 生成token，存储登录信息(json),利用处理后的UUID
        String token = UUID.randomUUID().toString().replace("-", "");
        // 处理登录
        // 设置ip,设置token,更新mysql
        findUser.setToken(LOGIN + token);
        // 执行添加或更新
        userRepository.save(findUser);
        // redis添加token信息和用户信息以及
        // 过期时间：pc端2小时，手机端1个月，手机端的H5也是2个小时
        // 1.pc 2.小程序
        if (findUser.getLoginType() == null || findUser.getLoginType() == 1) {
            //
            // TODO:过期时间：pc端2小时，手机端1个月，手机端的H5也是2个小时
            stringOps.set(LOGIN + token, JsonUtil.objectToString(findUser), 2, TimeUnit.HOURS);
        } else {
            stringOps.set(LOGIN + token, JsonUtil.objectToString(findUser), 30, TimeUnit.DAYS);
        }

        List<MenuVo> userMenuVoTree = getUserMenuVoTree(findUser);

        Map<String, Object> result = new HashMap<>(3);
        // 放入token
        result.put("token", token);
        result.put("menu", userMenuVoTree);
        result.put("user", findUser);
        return result;
    }

    @Override
    public User queryUserByToken(String token) throws BusinessException {
        ValueOperations<String, Object> stringOps = redisTemplate.opsForValue();
        Object o = stringOps.get(LOGIN + token);
        String result = null;
        if (o != null) {
            result = o.toString();
        }
        if (StringUtils.isEmpty(result)) {
            // 登录已过期，或未登录，请重新登录
            throw new BusinessException(ExceptionCode.TOKEN_FAILDE);
        }
        // 要更新失效时间
        redisTemplate.expire(LOGIN + token, 2, TimeUnit.HOURS);
        return JsonUtil.stringToObject(result, User.class);
    }

    @Override
    public List<Menu> getUserMenuTree(User user) throws BusinessException {
        // 获取用户的权限的菜单
        // 下面拼接权限菜单组成一个json格式
        return userMapper.findUserRoleMenu(user.getId());
    }

    @Override
    public List<MenuVo> getUserMenuVoTree(User user) throws BusinessException {
        // 获取用户的权限的菜单
        List<MenuVo> userRoleMenuAll = userMapper.findUserRoleMenuVo(user.getId());
        // 如果用户具有多种角色这个菜单需要去重
        // 通过set来去重
        // 放入set种可达到去重的目的
        Set<MenuVo> set = new HashSet<>(userRoleMenuAll);
        // 将去重后的set放入list中用于通过去重后的权限树得到权限菜单
        List<MenuVo> userRoleMenu = new ArrayList<>(set);
        // 将去重后的菜单按id重新排序
        Collections.sort(userRoleMenu);
        // 调用工具类生成新的权限菜单
        // 下面拼接权限菜单组成一个json格式的权限树
        return AuthTreeUtil.getOneMenu(userRoleMenu);
    }

    @Override
    public List<Role> findUserRole(Long id) {
        return userMapper.findUserRole(id);
    }

    @Override
    public Boolean setUserRole(Long userId, Long roleId) {
        return userMapper.insertUserRole(userId, roleId) == 1;
    }

}
